﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace PoolKit
{
    // TODO: Optimizations are possible (target of opportunity for next releases). We're locking _twice_ over the decorated collection and reconstructing Pooled<T>.
    internal class ResourceCollectionUsageTracking<TResource> : IResourceCollectionUsageTracking<TResource>
        where TResource : class
    {
        private readonly IResourceCollection<TResource> _resourceCollection;
        private readonly Dictionary<Pooled<TResource>, IStopwatch> _resourcesToLastUsageTime = new Dictionary<Pooled<TResource>, IStopwatch>();
        private readonly IStopwatchFactory _stopwatchFactory;
        private readonly DisposableCookie _disposableCookie = new DisposableCookie(Resources.ResourceCollectionDisposed);

        public ResourceCollectionUsageTracking(
            IResourceCollection<TResource> resourceCollection,
            IEnumerable<TResource> startingPoolOfResources,
            IStopwatchFactory stopwatchFactory)
        {
            if (resourceCollection == null) throw new ArgumentNullException("resourceCollection");
            if (startingPoolOfResources == null) throw new ArgumentNullException("startingPoolOfResources");
            if (stopwatchFactory == null) throw new ArgumentNullException("stopwatchFactory");

            _resourceCollection = resourceCollection;
            _stopwatchFactory = stopwatchFactory;

            foreach (TResource resource in startingPoolOfResources)
            {
                _resourcesToLastUsageTime[new Pooled<TResource>(resource)] = _stopwatchFactory.CreateAndStart();
            }
        }

        /// <summary>
        /// </summary>
        /// <param name="minumumResourceIdleTime"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public ReadOnlyCollection<TResource> GetResourcesSittingIdle(TimeSpan minumumResourceIdleTime)
        {
            if (minumumResourceIdleTime < TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException("minumumResourceIdleTime", Resources.NegativeTimeSpansNotAllowed);
            }

            var evictionCandidates = new List<TResource>();

            lock (_resourcesToLastUsageTime)
            {
                _disposableCookie.ThrowIfDisposed();

                foreach (KeyValuePair<Pooled<TResource>, IStopwatch> resourceToUsageTime in _resourcesToLastUsageTime)
                {
                    if (resourceToUsageTime.Value != null)
                    {
                        if (resourceToUsageTime.Value.Elapsed >= minumumResourceIdleTime)
                        {
                            evictionCandidates.Add(resourceToUsageTime.Key.Resource);
                        }
                    }
                }
            }

            return evictionCandidates.AsReadOnly();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <exception cref="ResourceCollectionEmptyException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public TimeSpan GetMaxResourceIdleTime()
        {
            TimeSpan maxIdleTime = TimeSpan.Zero;

            lock (_resourcesToLastUsageTime)
            {
                _disposableCookie.ThrowIfDisposed();

                if (_resourcesToLastUsageTime.Count == 0)
                {
                    throw new ResourceCollectionEmptyException(Resources.CannotGetMaxIdleTimeForResourceBecauseCollectionIsEmpty);
                }

                foreach (KeyValuePair<Pooled<TResource>, IStopwatch> resourceToUsageTime in _resourcesToLastUsageTime)
                {
                    if (resourceToUsageTime.Value != null && (resourceToUsageTime.Value.Elapsed > maxIdleTime))
                    {
                        maxIdleTime = resourceToUsageTime.Value.Elapsed;
                    }
                }
            }

            return maxIdleTime;
        }

        /// <summary>
        /// Tries to take a resource out of resource collection. If this succeeds, the collection still keeps track of the resource,
        /// as it can be returned back to it.
        /// </summary>
        /// <param name="resource"></param>
        /// <returns></returns>
        /// <exception cref="ObjectDisposedException"></exception>
        public bool TryTake(out TResource resource)
        {
            bool taken = _resourceCollection.TryTake(out resource);

            if (taken)
            {
                lock (_resourcesToLastUsageTime)
                {
                    // TODO: use the Null object pattern? -- target of opportunity for next releases
                    _resourcesToLastUsageTime[new Pooled<TResource>(resource)] = null;
                }
            }

            return taken;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="resource"></param>
        /// <exception cref="CannotAcceptResourceException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void AcceptBack(TResource resource)
        {
            _resourceCollection.AcceptBack(resource);

            lock (_resourcesToLastUsageTime)
            {
                _resourcesToLastUsageTime[new Pooled<TResource>(resource)] = _stopwatchFactory.CreateAndStart();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="resourceBeingSubstituted"></param>
        /// <param name="substitution"></param>
        /// <exception cref="CannotAcceptResourceException"></exception>
        /// <exception cref="ReplacingResourceProducesDuplicateException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void AcceptSubstitution(TResource resourceBeingSubstituted, TResource substitution)
        {
            _resourceCollection.AcceptSubstitution(resourceBeingSubstituted, substitution);

            lock (_resourcesToLastUsageTime)
            {
                _resourcesToLastUsageTime.Remove(new Pooled<TResource>(resourceBeingSubstituted));
                _resourcesToLastUsageTime[new Pooled<TResource>(substitution)] = _stopwatchFactory.CreateAndStart();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="newResource"></param>
        /// <exception cref="CannotAddResourceException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void Add(TResource newResource)
        {
            _resourceCollection.Add(newResource);

            lock (_resourcesToLastUsageTime)
            {
                _resourcesToLastUsageTime[new Pooled<TResource>(newResource)] = _stopwatchFactory.CreateAndStart();
            }
        }

        /// <summary>
        /// Note: it's assumed that after calling this one, the pool will actually call AcceptBack or AcceptBackBroken.
        /// </summary>
        /// <param name="resource"></param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void ReserveRightToReturnResource(TResource resource)
        {
            _resourceCollection.ReserveRightToReturnResource(resource);
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="resourceToEvict"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public bool TryEvict(TResource resourceToEvict)
        {
            bool removed = _resourceCollection.TryEvict(resourceToEvict);

            // Note: locking gaps seem to not be hurting the correctness here and in other methods.

            if (removed)
            {
                lock (_resourcesToLastUsageTime)
                {
                    _resourcesToLastUsageTime.Remove(new Pooled<TResource>(resourceToEvict));
                }
            }

            return removed;
        }

        /// <summary>
        /// Register a resource making it look like it had been in collection at some point
        /// and has been taken from it.
        /// </summary>
        /// <param name="resource"></param>
        /// <exception cref="CannotAddResourceException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void RegisterResourceAsTaken(TResource resource)
        {
            _resourceCollection.RegisterResourceAsTaken(resource);
        }

        /// <summary>
        /// Makes the collection forget the resource that is currently out of collection.
        /// </summary>
        /// <param name="resource"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="CannotForgetResourceException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void Forget(TResource resource)
        {
            _resourceCollection.Forget(resource);

            lock (_resourcesToLastUsageTime)
            {
                _resourcesToLastUsageTime.Remove(new Pooled<TResource>(resource));
            }
        }

        public int Count
        {
            get
            {
                return _resourceCollection.Count;
            }
        }

        public event EventHandler ItemCountUp
        {
            add { _resourceCollection.ItemCountUp += value; }
            remove { _resourceCollection.ItemCountUp -= value; }
        }

        public event EventHandler ItemCountDown
        {
            add { _resourceCollection.ItemCountDown += value; }
            remove { _resourceCollection.ItemCountDown -= value; }
        }

        public void Dispose()
        {
            lock (_resourcesToLastUsageTime)
            {
                if (_disposableCookie.IsDisposed)
                {
                    return;
                }

                _resourceCollection.Dispose();

                _resourcesToLastUsageTime.Clear();

                _disposableCookie.MarkAsDisposed();
            }
        }
    }
}